package com.dustedpixels.jasmin.chips.z80.util;

import java.util.Random;

import com.dustedpixels.jasmin.chips.z80.Z80;
import com.dustedpixels.jasmin.chips.z80.Z80Context;
import com.dustedpixels.jasmin.chips.z80.Z80Factory;
import com.dustedpixels.jasmin.chips.z80.impl.Z80Version1Factory;
import com.dustedpixels.jasmin.chips.z80.impl.Z80VersionQAOPFactory;

/**
 * Compares two implementations of Z80 and prints comparision report.
 */
public class Z80Comparator {
    private static final int TESTS = 1000;
    
    private final Random seedRandom = new Random();

    private Z80StatePrinter statePrinter = new Z80StatePrinter();
    private Z80StateComparator stateComparator = new Z80StateComparator();
    private Z80RandomStateGenerator randomStateGenerator = new Z80RandomStateGenerator();

    /**
     * Compares two instances of Z80 obtained through specified factories.
     * 
     * @param factoryA The first factory
     * @param factoryB The second factory
     */
    public void compare(Z80Factory factoryA, Z80Factory factoryB) {
        final int[] mem = new int[5];

        Z80Context context = new StubZ80Context() {
            public byte peek(short address) {
                int index = address & 0xFFFF;
                
                if (index < mem.length) {
                    return (byte) mem[index];
                } else {
                    return 0x27;
                }
            }
        };

        Z80 z80A = factoryA.createZ80(context);
        Z80 z80B = factoryB.createZ80(context);

        // Test basic instruction set.
        for (int i = 0; i <= 0xFF; i++) {
            // Skip extended instructions.
            if (i == 0xCB || i == 0xDD || i == 0xED || i == 0xFD)
                continue;

            for (int j = 0; j < TESTS; j++) {
                // Initialize instruction
                mem[0] = i;
                mem[1] = seedRandom.nextInt() & 0xFF;
                mem[2] = seedRandom.nextInt() & 0xFF;
                mem[3] = seedRandom.nextInt() & 0xFF;
                mem[4] = seedRandom.nextInt() & 0xFF;

                // Initialize Z80 chips with the same random content.
                long seed = seedRandom.nextLong();

                randomStateGenerator.generateRandomState(z80A, seed);
                randomStateGenerator.generateRandomState(z80B, seed);

                z80A.setPC((short) 0);
                z80B.setPC((short) 0);
                
                // Process one instruction
                z80A.processInstruction();
                z80B.processInstruction();

                // Compare all registers.
                if (!stateComparator.areStatesEqual(z80A, z80B)) {
                    System.out.println("@@@ Difference on instruction " + 
                            Integer.toHexString(i));
                    printStates(z80A, z80B);
                    j = TESTS;
                }
            }
        }

        // Test CB instruction set.
        for (int i = 0; i <= 0xFF; i++) {
            for (int j = 0; j < TESTS; j++) {
                // Initialize instruction
                mem[0] = 0xCB;
                mem[1] = i;
                mem[2] = seedRandom.nextInt() & 0xFF;
                mem[3] = seedRandom.nextInt() & 0xFF;
                mem[4] = seedRandom.nextInt() & 0xFF;

                // Initialize Z80 chips with the same random content.
                long seed = seedRandom.nextLong();

                randomStateGenerator.generateRandomState(z80A, seed);
                randomStateGenerator.generateRandomState(z80B, seed);

                z80A.setPC((short) 0);
                z80B.setPC((short) 0);
                
                // Process one instruction
                z80A.processInstruction();
                z80B.processInstruction();

                // Compare all registers.
                if (!stateComparator.areStatesEqual(z80A, z80B)) {
                    System.out.println("@@@ Difference on instruction " + 
                            Integer.toHexString(0xCB00 | i));
                    printStates(z80A, z80B);
                    j = TESTS;
                }
            }
        }

        // Test ED instruction set.
        for (int i = 0; i <= 0xFF; i++) {
            // Skip extended instructions.
            for (int j = 0; j < TESTS; j++) {
                // Initialize instruction
                mem[0] = 0xED;
                mem[1] = i;
                mem[2] = seedRandom.nextInt() & 0xFF;
                mem[3] = seedRandom.nextInt() & 0xFF;
                mem[4] = seedRandom.nextInt() & 0xFF;

                // Initialize Z80 chips with the same random content.
                long seed = seedRandom.nextLong();

                randomStateGenerator.generateRandomState(z80A, seed);
                randomStateGenerator.generateRandomState(z80B, seed);

                z80A.setPC((short) 0);
                z80B.setPC((short) 0);
                
                // Process one instruction
                z80A.processInstruction();
                z80B.processInstruction();

                // Compare all registers.
                if (!stateComparator.areStatesEqual(z80A, z80B)) {
                    System.out.println("@@@ Difference on instruction " + 
                            Integer.toHexString(0xED00 | i));
                    printStates(z80A, z80B);
                    j = TESTS;
                }
            }
        }

        // Test DD instruction set.
        for (int i = 0; i <= 0xFF; i++) {
            // Skip extended instructions.
            if (i == 0xCB || i == 0xDD || i == 0xED || i == 0xFD)
                continue;
            
            for (int j = 0; j < TESTS; j++) {
                // Initialize instruction
                mem[0] = 0xDD;
                mem[1] = i;
                mem[2] = seedRandom.nextInt() & 0xFF;
                mem[3] = seedRandom.nextInt() & 0xFF;
                mem[4] = seedRandom.nextInt() & 0xFF;

                // Initialize Z80 chips with the same random content.
                long seed = seedRandom.nextLong();

                randomStateGenerator.generateRandomState(z80A, seed);
                randomStateGenerator.generateRandomState(z80B, seed);

                z80A.setPC((short) 0);
                z80B.setPC((short) 0);
                
                // Process one instruction
                z80A.processInstruction();
                z80B.processInstruction();

                // Compare all registers.
                if (!stateComparator.areStatesEqual(z80A, z80B)) {
                    System.out.println("@@@ Difference on instruction " + 
                            Integer.toHexString(0xDD00 | i));
                    printStates(z80A, z80B);
                    j = TESTS;
                }
            }
        }

        // Test FD instruction set.
        for (int i = 0; i <= 0xFF; i++) {
            // Skip extended instructions.
            if (i == 0xCB || i == 0xDD || i == 0xED || i == 0xFD)
                continue;
            
            for (int j = 0; j < TESTS; j++) {
                // Initialize instruction
                mem[0] = 0xFD;
                mem[1] = i;
                mem[2] = seedRandom.nextInt() & 0xFF;
                mem[3] = seedRandom.nextInt() & 0xFF;
                mem[4] = seedRandom.nextInt() & 0xFF;

                // Initialize Z80 chips with the same random content.
                long seed = seedRandom.nextLong();

                randomStateGenerator.generateRandomState(z80A, seed);
                randomStateGenerator.generateRandomState(z80B, seed);

                z80A.setPC((short) 0);
                z80B.setPC((short) 0);
                
                // Process one instruction
                z80A.processInstruction();
                z80B.processInstruction();

                // Compare all registers.
                if (!stateComparator.areStatesEqual(z80A, z80B)) {
                    System.out.println("@@@ Difference on instruction " + 
                            Integer.toHexString(0xFD00 | i));
                    printStates(z80A, z80B);
                    j = TESTS;
                }
            }
        }

        // Test DDCB instruction set.
        // TODO: Only documented ones.
        for (int i = 0x06; i <= 0xFF; i+=0x08) {
            for (int j = 0; j < TESTS; j++) {
                // Initialize instruction
                mem[0] = 0xDD;
                mem[1] = 0xCB;
                mem[2] = seedRandom.nextInt() & 0xFF;
                mem[3] = i;
                mem[4] = seedRandom.nextInt() & 0xFF;

                // Initialize Z80 chips with the same random content.
                long seed = seedRandom.nextLong();

                randomStateGenerator.generateRandomState(z80A, seed);
                randomStateGenerator.generateRandomState(z80B, seed);

                z80A.setPC((short) 0);
                z80B.setPC((short) 0);

                //System.out.println("Before:::");
                //printStates(z80A, z80B);

                
                // Process one instruction
                z80A.processInstruction();
                z80B.processInstruction();

                // Compare all registers.
                if (!stateComparator.areStatesEqual(z80A, z80B)) {
                    System.out.println("@@@ Difference on instruction " + 
                            Integer.toHexString(0xDDCB00 | i));
                    printStates(z80A, z80B);
                    j = TESTS;
                }
            }
        }

        // Test FDCB instruction set.
        // TODO: Only documented ones.
        for (int i = 0x06; i <= 0xFF; i+=0x08) {
            for (int j = 0; j < TESTS; j++) {
                // Initialize instruction
                mem[0] = 0xFD;
                mem[1] = 0xCB;
                mem[2] = seedRandom.nextInt() & 0xFF;
                mem[3] = i;
                mem[4] = seedRandom.nextInt() & 0xFF;

                // Initialize Z80 chips with the same random content.
                long seed = seedRandom.nextLong();

                randomStateGenerator.generateRandomState(z80A, seed);
                randomStateGenerator.generateRandomState(z80B, seed);

                z80A.setPC((short) 0);
                z80B.setPC((short) 0);

                //System.out.println("Before:::");
                //printStates(z80A, z80B);

                
                // Process one instruction
                z80A.processInstruction();
                z80B.processInstruction();

                // Compare all registers.
                if (!stateComparator.areStatesEqual(z80A, z80B)) {
                    System.out.println("@@@ Difference on instruction " + 
                            Integer.toHexString(0xFDCB00 | i));
                    printStates(z80A, z80B);
                    j = TESTS;
                }
            }
        }
    }

    private void printStates(Z80 z80A, Z80 z80B) {
        System.out.println("==== Z80A ====");
        statePrinter.printState(z80A);

        System.out.println("==== Z80B ====");
        statePrinter.printState(z80B);
    }

    public static void main(String[] args) {
        Z80Comparator comparator = new Z80Comparator();

        Z80Factory factoryA = new Z80Version1Factory();
        Z80Factory factoryB = new Z80VersionQAOPFactory();

        comparator.compare(factoryA, factoryB);
    }
}
